package com.googlecode.maurersmarbles;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.googlecode.maurersmarbles.drawables.Drawable;
import com.googlecode.maurersmarbles.drawables.ImageBoardDrawable;
import com.googlecode.maurersmarbles.drawables.ImageCardDrawable;
import com.googlecode.maurersmarbles.drawables.ImageMarbleDrawable;
import com.googlecode.maurersmarbles.drawables.SimpleBoardDrawable;
import com.googlecode.maurersmarbles.drawables.SimpleCardDrawable;
import com.googlecode.maurersmarbles.drawables.SimpleDealerChipDrawable;
import com.googlecode.maurersmarbles.drawables.SimpleMarbleDrawable;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class GameView extends SurfaceView implements SurfaceHolder.Callback {

	/**
	 * The scale size of the components compared to the canvas.
	 */
	private static final float COMPONENT_SCALE_FACTOR = 0.95f;
	/**
	 * The game board.
	 */
	private Board board;
	/**
	 * Whether or not we are executing the other players.
	 */
	private boolean executingOtherPlayers = false;
	/**
	 * The discarded card.
	 */
	private Card discardedCard = null;
	/**
	 * The currently selected card.
	 */
	private Card selectedCard = null;
	/**
	 * The cards display used to display the player cards.
	 */
	private CardsDisplay cardsDisplay;
	/**
	 * The game state.
	 */
	private GameState gameState;
	/**
	 * The selected marble used for compound plays.
	 */
	private Marble selectedMarble = null;
	/**
	 * The surface holder.
	 */
	private SurfaceHolder surfaceHolder;

	/**
	 * Creates a new GameView.
	 * @param context the context
	 * @param attributeSet the attribute set
	 */
	public GameView(Context context, AttributeSet attributeSet) {

		super(context, attributeSet);

		ImageBoardDrawable.setContext(context);
		ImageCardDrawable.setContext(context);
		ImageMarbleDrawable.setContext(context);

		board = new Board();
		cardsDisplay = new LinearCardsDisplay();

		// register our interest in hearing about changes to our surface
		surfaceHolder = getHolder();
		surfaceHolder.addCallback(this);

		newGame();

	}

	private void advancePlayersTurn() {

		// check to see if the game is over
		if (GameLogic.isGameOver(gameState)) {
			// TODO Add game over logic
			return;
		}

		int emptyCardCount = 0;
		for (Player player : gameState.getPlayers()) {
			if (player.getCards().isEmpty()) {
				emptyCardCount++;
			}
		}

		if (emptyCardCount == GameLogic.NUMBER_OF_PLAYERS) {
			// deal next hand, current players turn is dealer + 1
			GameLogic.dealCards(gameState);
		}
		else {
			// advance the player turn to the next player
			gameState.setCurrentPlayersTurn(GameLogic
					.getNextPlayerNumber(gameState.getCurrentPlayersTurn()));
		}

	}

	/**
	 * Animates the card being played.
	 * @param card the card to play
	 * @param duration the duration of the animation
	 */
	private void animateCardBeingPlayed(Card card, Point[] sourceCorners,
			long duration) {

		if (sourceCorners != null) {

			Point[] destinationCorners = board.getDiscardCornersOnScreen();

			Long currentTime;
			Long startTime = currentTime = System.currentTimeMillis();

			while (currentTime < (startTime + duration)) {

				double weight =
						(double) (currentTime - startTime) / (double) duration;

				Point corners[] = new Point[4];
				corners[0] =
						Util.interpolate(sourceCorners[0],
								destinationCorners[0], weight);
				corners[1] =
						Util.interpolate(sourceCorners[1],
								destinationCorners[1], weight);
				corners[2] =
						Util.interpolate(sourceCorners[2],
								destinationCorners[2], weight);
				corners[3] =
						Util.interpolate(sourceCorners[3],
								destinationCorners[3], weight);

				Map<Object, Drawable> drawables = getDrawables();
				// remove the item so we change the draw order with this on top
				drawables.remove(card);
				drawables.put(card, getCardDrawable(card, corners, false));
				redraw(drawables);

				currentTime = System.currentTimeMillis();

			}

		}

	}

	private void animateMarbleMove(Move move, long duration) {

		Long currentTime;
		Long startTime = currentTime = System.currentTimeMillis();

		Marble marble =
				gameState.getPlayers()[move.getPlayerNumber()].getMarble(move
						.getMarbleNumber());

		Point gridCellCurrent =
				board.getGridCellForIndex(marble.getBoardIndex());
		Point gridCellNext = board.getGridCellForIndex(move.getNewBoardIndex());

		while (currentTime < (startTime + duration)) {

			double weight =
					(double) (currentTime - startTime) / (double) duration;

			double gridCellX =
					gridCellCurrent.x
							+ ((gridCellNext.x - gridCellCurrent.x) * weight);
			double gridCellY =
					gridCellCurrent.y
							+ ((gridCellNext.y - gridCellCurrent.y) * weight);
			Rect boundingBox =
					board.getBoundingBox(gridCellX, gridCellY,
							board.getMarbleSize());

			Map<Object, Drawable> drawables = getDrawables();
			// remove the item so we change the draw order with this on top
			drawables.remove(marble);
			drawables.put(
					marble,
					getMarbleDrawable(
							gameState.playerColors[move.getPlayerNumber()],
							boundingBox));
			redraw(drawables);

			currentTime = System.currentTimeMillis();

		}

	}

	/**
	 * Executes the players turns.
	 */
	private void executePlayerTurns() {

		executingOtherPlayers = true;

		boolean continueExecuting = true;
		while (continueExecuting) {

			Player currentPlayer =
					gameState.getPlayers()[gameState.getCurrentPlayersTurn()];

			// check to see if the player has cards
			if (!currentPlayer.getCards().isEmpty()) {

				// get the list of valid plays
				List<Play> plays = GameLogic.getPlays(gameState, currentPlayer);
				if (!plays.isEmpty()) {

					// TODO Change this duration
					try {
						Thread.sleep(500);
					}
					catch (InterruptedException e) {
						e.printStackTrace();
					}

					// TODO select the best play to execute
					Play play = plays.get(0);
					executePlay(play);

				}
				else {

					// no plays, throw in cards
					currentPlayer.getCards().clear();

				}

			}

			// advance to the next players turn
			advancePlayersTurn();

			if (gameState.getCurrentPlayersTurn() == GameLogic.USER_PLAYER_NUMBER) {

				if (!gameState.getPlayers()[gameState.getCurrentPlayersTurn()]
						.getCards().isEmpty()) {
					continueExecuting = false;
				}
				else {
					advancePlayersTurn();
				}

			}

		}

		executingOtherPlayers = false;

	}

	private void executePlay(Play play) {

		// animate the card being played
		animateCardBeingPlayed(play.getCard(),
				getCardCornersForPlayer(gameState.getCurrentPlayersTurn()),
				MaurersMarbles.getAnimationTime());

		// update the game state
		discardedCard = play.getCard();
		GameLogic.applyCardBeingPlayed(gameState, play.getCard());
		redraw();

		// TODO Change this duration
		if (gameState.getCurrentPlayersTurn() != GameLogic.USER_PLAYER_NUMBER) {
			try {
				Thread.sleep(500);
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		// execute the marble moves
		for (Move move : play.getMoves()) {

			// animate the marble being moved
			animateMarbleMove(move, MaurersMarbles.getAnimationTime());

			// update the marble state
			GameLogic.applyMove(gameState, move);

		}

		redraw();

	}

	/**
	 * Returns a new board drawable.
	 * @param board the board to draw
	 * @param playerColors the array that contains the player colors
	 * @return a new board drawable
	 */
	private Drawable getBoardDrawable(Board board, int[] playerColors) {
		// TODO Update to be dynamic based on the selected theme
		return new ImageBoardDrawable(board, playerColors);
		// return new SimpleBoardDrawable(board, playerColors);
	}

	/**
	 * Returns the card corners for the given player.
	 * @param player the player number
	 * @return the card corners for the given player
	 */
	private Point[] getCardCornersForPlayer(int player) {

		double cardHeight = cardsDisplay.getCardHeight();
		double cardWidth = cardsDisplay.getCardWidth();

		Rect rect =
				board.getBoundingBox(
						board.getGridCellForIndex(Board.HOME_ENTRY[player]),
						0.0);
		int centerX = rect.centerX();
		int centerY = rect.centerY();

		Point[] corners = new Point[4];
		corners[0] =
				new Point(centerX - (int) (cardWidth / 2.0), centerY
						- (int) (cardHeight / 2.0));
		corners[1] =
				new Point(centerX + (int) (cardWidth / 2.0), centerY
						- (int) (cardHeight / 2.0));
		corners[2] =
				new Point(centerX + (int) (cardWidth / 2.0), centerY
						+ (int) (cardHeight / 2.0));
		corners[3] =
				new Point(centerX - (int) (cardWidth / 2.0), centerY
						+ (int) (cardHeight / 2.0));

		return corners;

	}

	/**
	 * Returns a new card drawable.
	 * @param card the card
	 * @param corners the corners of the card
	 * @param selected whether or not the card is selected
	 * @return a new card drawable
	 */
	public Drawable getCardDrawable(Card card, Point[] corners, boolean selected) {
		// TODO Update to be dynamic based on the selected theme
		return new ImageCardDrawable(card, corners, selected);
		// return new SimpleCardDrawable(card, corners, selected);
	}

	/**
	 * Returns a new dealer chip drawable.
	 * @param boundingBox the bounding box to draw the dealer chip in
	 * @return a new dealer chip drawable
	 */
	public Drawable getDealerChipDrawable(Rect boundingBox) {
		// TODO Update to be dynamic based on the selected theme
		return new SimpleDealerChipDrawable(boundingBox);
	}

	/**
	 * Returns the map that contains the drawables to draw. The key into the map
	 * is the object that the drawable is drawing and the value is the drawable.
	 * @return the map that contains the drawables to draw
	 */
	private Map<Object, Drawable> getDrawables() {

		Map<Object, Drawable> drawables = new LinkedHashMap<Object, Drawable>();

		// add the game board
		drawables.put(board, getBoardDrawable(board, gameState.playerColors));

		// add the discarded card
		if (discardedCard != null) {
			Point[] corners = board.getDiscardCornersOnScreen();
			drawables.put(discardedCard,
					getCardDrawable(discardedCard, corners, false));
		}

		// add the dealer chip
		drawables
				.put(new Object(), getDealerChipDrawable(board
						.getDealerChipRect(gameState.dealer)));

		// add the marbles
		for (Player player : gameState.getPlayers()) {
			for (Marble marble : player.getMarbles()) {
				Rect boundingBox =
						board.getBoundingBox(board.getGridCellForIndex(marble
								.getBoardIndex()), board.getMarbleSize());
				drawables
						.put(marble,
								getMarbleDrawable(gameState.playerColors[player
										.getPlayerNumber()], boundingBox));
			}
		}

		// add the player's cards
		cardsDisplay
				.setCards(gameState.getPlayers()[GameLogic.USER_PLAYER_NUMBER]
						.getCards());
		for (Card card : gameState.getPlayers()[GameLogic.USER_PLAYER_NUMBER]
				.getCards()) {
			Point[] corners = cardsDisplay.getCardCornersOnScreen(card);
			drawables.put(card,
					getCardDrawable(card, corners, card == selectedCard));
		}

		return drawables;

	}

	/**
	 * Returns the marble at the given screen x/y location, null if no marble.
	 * @param players the players array
	 * @param x the x location on the screen
	 * @param y the y location on the screen
	 * @return the marble at the given screen x/y location, null if no marble
	 */
	public Marble getMarbleAtScreenXY(Player[] players, int x, int y) {
		Point point = board.getGridCellAtScreenXY(x, y);
		if (point != null) {
			int index = board.getIndexForGridCell(point);
			for (Player player : players) {
				for (Marble marble : player.getMarbles()) {
					if (marble.getBoardIndex() == index) {
						return marble;
					}
				}
			}
		}
		return null;
	}

	/**
	 * Returns a new marble drawable.
	 * @param color the color of the marble
	 * @param boundingBox the bounding box to draw the marble in
	 * @return a new marble drawable
	 */
	public Drawable getMarbleDrawable(int color, Rect boundingBox) {
		// TODO Update to be dynamic based on the selected theme
		return new ImageMarbleDrawable(color, boundingBox);
		// return new SimpleMarbleDrawable(color, boundingBox);
	}

	/**
	 * Redraws the game screen.
	 */
	private void redraw() {
		redraw(getDrawables());
	}

	/**
	 * Redraws the game screen
	 * @param drawables the list of drawables to draw
	 */
	private void redraw(Map<Object, Drawable> drawables) {

		Canvas canvas = null;
		try {

			canvas = surfaceHolder.lockCanvas(null);
			synchronized (surfaceHolder) {

				// clear the screen
				canvas.drawRGB(0, 0, 0);

				// draw all of the drawables
				for (Map.Entry<Object, Drawable> entry : drawables.entrySet()) {
					entry.getValue().draw(canvas);
				}

				// TODO Move this to a status panel
				int x = board.getOuterRect().left;
				int y = board.getOuterRect().bottom + 50;
				Paint paint = new Paint();
				paint.setTextSize(50);
				paint.setColor(Color.WHITE);
				String text = "";
				for (Player player : gameState.getPlayers()) {
					text +=
							"Player" + player.getPlayerNumber() + ": "
									+ player.getCards().size() + "      ";
				}
				text += "Turn: " + gameState.getCurrentPlayersTurn();
				canvas.drawText(text, x, y, paint);

			}

		}
		finally {
			if (canvas != null) {
				surfaceHolder.unlockCanvasAndPost(canvas);
			}
		}

	}

	@Override
	public boolean onTouchEvent(MotionEvent e) {

		// prevent the user from interacting while we are executing the other
		// players
		if (executingOtherPlayers) {
			return true;
		}

		if (e.getAction() == MotionEvent.ACTION_UP) {

			// needed to start the game
			if (gameState.getCurrentPlayersTurn() != GameLogic.USER_PLAYER_NUMBER) {
				executePlayerTurns();
				return true;
			}

			List<Play> validPlays =
					GameLogic.getPlays(gameState, GameLogic.USER_PLAYER_NUMBER);
			if (validPlays.isEmpty()) {

				// no plays, throw in cards
				gameState.getPlayers()[GameLogic.USER_PLAYER_NUMBER].getCards()
						.clear();
				redraw();
				advancePlayersTurn();
				executePlayerTurns();
				return true;

			}

			Card card =
					cardsDisplay.getCardAtScreenXY((int) e.getX(),
							(int) e.getY());
			if (card != null) {
				if (card == selectedCard) {
					selectedCard = null;
				}
				else {
					selectedCard = card;
				}
				selectedMarble = null;
				redraw();
				return true;
			}

			if (selectedCard != null) {

				Marble marble =
						getMarbleAtScreenXY(gameState.getPlayers(),
								(int) e.getX(), (int) e.getY());
				if (marble != null) {

					// TODO Need to update for playing a 7
					if (selectedCard.getId() == Card.CARD_ID_J
							&& selectedMarble == null) {
						selectedMarble = marble;
						// TODO Highlight marble?
						return true;
					}

					Play userPlay = null;
					for (Play play : validPlays) {
						if (play.getCard() == selectedCard) {
							// TODO Need to update this for playing a 7
							if (selectedCard.getId() == Card.CARD_ID_J) {
								if (play.isSamePlay(
										selectedMarble.getPlayerNumber(),
										selectedMarble.getMarbleNumber(),
										marble.getPlayerNumber(),
										marble.getMarbleNumber())) {
									userPlay = play;
									break;
								}
							}
							else {
								if (play.isSamePlay(marble.getPlayerNumber(),
										marble.getMarbleNumber())) {
									userPlay = play;
									break;
								}
							}
						}
					}

					if (userPlay != null) {
						executePlay(userPlay);
						advancePlayersTurn();
						executePlayerTurns();
						return true;
					}
					else {
						// user made an invalid play
						selectedCard = null;
						selectedMarble = null;
						redraw();
						return true;
					}

				}

			}

		}

		return true;

	}

	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width,
			int height) {

		synchronized (surfaceHolder) {

			boolean isPortrait = width <= height;

			// compute the board size and location
			int boardSize =
					(int) ((isPortrait ? width : height) * COMPONENT_SCALE_FACTOR);
			int boardStartXY =
					(int) (((isPortrait ? width : height) - boardSize) / 2.0);
			board.setLocationAndSize(boardStartXY, boardStartXY, boardSize);

			// compute the cards display size and location
			int cardsDisplayWidth =
					(int) ((isPortrait ? width : width - height) * COMPONENT_SCALE_FACTOR);
			int cardsDisplayHeight =
					(int) ((isPortrait ? height - width : height) * COMPONENT_SCALE_FACTOR);
			int cardsDisplayStartX =
					(int) (isPortrait ? (width - cardsDisplayWidth) / 2.0
							: height + ((width - height) - cardsDisplayWidth)
									/ 2.0);
			int cardsDisplayStartY =
					(int) (isPortrait ? width
							+ (((height - width) - cardsDisplayHeight) / 2.0)
							: (height - cardsDisplayHeight) / 2.0);
			cardsDisplay.setLocationAndSize(cardsDisplayStartX,
					cardsDisplayStartY, cardsDisplayWidth, cardsDisplayHeight);

		}

		redraw();

	}

	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		redraw();
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {

	}

	public void newGame() {

		// create a new game state
		gameState = new GameState();

		// TODO Change to let the user select the color
		gameState.setUserPlayerColor(0);

		// TODO Determine dealer
		gameState.dealer = -1;
		GameLogic.dealCards(gameState);

	}

}
